package simplechat;

// This file contains material supporting section 3.7 of the textbook:
// "Object Oriented Software Engineering" and is issued under the open-source
// license found at www.lloseng.com

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.Scanner;

import ocsf.server.AbstractServer;
import ocsf.server.ConnectionToClient;

/**
 * This class overrides some of the methods in the abstract
 * superclass in order to give more functionality to the server.
 *
 * @author Dr Timothy C. Lethbridge
 * @author Dr Robert Lagani&egrave;re
 * @author Fran&ccedil;ois B&eacute;langer
 * @author Paul Holden
 * @version July 2000
 */
public class EchoServer extends AbstractServer
{
  //Class variables *************************************************

  /**
   * The default port to listen on.
   */
  final public static int DEFAULT_PORT = 5555;
  final public static String FILENAME = "/home/shokora/nicks.dat";
  
  private NickServDB nickServDB;
  private HashMap<String,Boolean> currentNicknames = new HashMap<String,Boolean>();
  private Random generator = new Random(1296393);

  //Constructors ****************************************************

  /**
   * Constructs an instance of the echo server.
   *
   * @param port The port number to connect on.
   */
  public EchoServer(int port)
  {
    super(port);
    
    try
    {
    	FileInputStream in = new FileInputStream(FILENAME);
    	ObjectInputStream inObj = new ObjectInputStream(in);
    	nickServDB = (NickServDB) inObj.readObject();
    }
    catch(IOException e)
    {
    	nickServDB = new NickServDB();
    }
    catch(ClassNotFoundException e)
    {
    	e.printStackTrace();
    	System.exit(0);
    }
  }


  //Instance methods ************************************************

  /**
   * This method handles any messages received from the client.
   *
   * @param msg The message received from the client.
   * @param client The connection from which the message originated.
   */
  public void handleMessageFromClient(Object msg, ConnectionToClient client)
  {
	String commandString = String.valueOf(msg);
	Scanner scanner = new Scanner(commandString);
	ArrayList<String> params = new ArrayList<String>();
	String nickname = client.getNickName();
	
	//First 'param' is the actual command, the rest are parameters, C style
	while(scanner.hasNext())
	{
		params.add(scanner.next());
	}
	
	String command = params.get(0);
	command = command.substring(1).toUpperCase();
	
	try
	{
		/**
		 * No further parameters
		 */
		if(command.equals("REGISTER"))
		{
			if(nickServDB.checkIfRegistered(nickname))
			{
				client.sendToClient("You cannot register an already registered nickname, unregister first");
			}
			else
			{
				client.setPasswordVerify(false); //If someone calls register in the middle of registering
				client.sendToClient("Please enter a password: ");
			}
		}
		
		/**
		 * param[1] == password
		 */
		if(command.equals("PASSWORD"))
		{
			if(client.getPasswordVerify() && client.getPasswordToBeVerified().equals(params.get(1)))
			{
				this.nickServDB.createInitialAccount(nickname, params.get(1));
				client.setAuthenticated(true);
				client.setPasswordVerify(false);
				client.sendToClient("Successfully registered nickname, you are now logged in.");
			}
			else if(client.getPasswordVerify() && !client.getPasswordToBeVerified().equals(params.get(1)))
			{
				client.sendToClient("Password is not the same, please try again");
				client.setPasswordVerify(false);
			}
			else if(!client.getPasswordVerify())
			{
				client.setPasswordVerify(true);
				client.setPasswordToBeVerified(params.get(1));
				client.sendToClient("Please enter the password again to verify");
			}
		}
		
		/**
		 * param[1] == password
		 */
		if(command.equals("AUTHENTICATE"))
		{
			if(!client.isAuthenticated())
			{
				if(this.nickServDB.checkPassword(nickname, params.get(1)))
				{
					client.setAuthenticated(true);
					client.sendToClient("Successfully authenticated, hello"+nickname);
				}
				else
				{
					String randomNickname = generateRandomNickname();
					currentNicknames.remove(nickname);
					client.setNickName(randomNickname);
					currentNicknames.put(client.getNickName(),true);
					client.sendToClient("Wrong password, your nickname has been reset to "+randomNickname);
				}
			}
			else
			{
				client.sendToClient("You are already authenticated");
			}
		}
		
		/**
		 * param[1] == nickname to unregister, optional if not specified, current nickname will be used
		 */
		if(command.equals("UNREGISTER"))
		{
			if(!client.isAuthenticated())
			{
				client.sendToClient("You can only use that command if you are authenticated.");
			}
			else if(this.nickServDB.checkIfRegistered(nickname))
			{
				if(params.size() > 1)
				{
					if(this.nickServDB.checkIfOwnerOfNick(nickname, params.get(1)))
					{
						this.nickServDB.deleteNicknameFromAccount(params.get(1));
						client.sendToClient("Nickname successfully unregistered");
					}
					else
					{
						client.sendToClient("You are not the owner of that nickname");
					}
				}
				else
				{
					this.nickServDB.deleteNicknameFromAccount(nickname);
					client.sendToClient("Nickname successfully unregistered");
				}
			}
			else
			{
				client.sendToClient("You cannot unregister a nickname that is not registered");
			}
		}
		
		/**
		 * Log the user out by deauthenticating him and renaming him to a random nickname
		 */
		if(command.equals("LOGOUT"))
		{
			client.setAuthenticated(false);
			String nicknameRandom = generateRandomNickname();
			changeNickname(client, nickname, nicknameRandom);
			client.sendToClient("Successfully logged out, your nickname has been reset to "+nicknameRandom);
		}
		
		/**
		 * When no command is issued by the user, chat is used for server-wide chat.
		 */
		if(command.equals("CHAT"))
		{
			String message = commandString.substring(command.length()+1);
		    System.out.println("Message received: " + message + " from " + client.getNickName());
		    this.sendToAllClients("<"+client.getNickName()+"> "+message);
		}
		
		/**
		 * Nickname in the parameter will be merged with the current logged in account if the password is correct for that nickname.
		 * You have to be authenticated to use this.
		 * 
		 * Params[1] == nickname
		 * Params[2] == password
		 */
		if(command.equals("MERGE"))
		{
			if(client.isAuthenticated())
			{
				if(nickServDB.checkIfRegistered(nickname))
				{
					if(nickServDB.checkPassword(params.get(1), params.get(2)))
					{
						nickServDB.changeAccountNumber(params.get(1), nickServDB.getAccountNumber(nickname));
					}
					else
					{
						client.sendToClient("Password for nickname "+params.get(1)+" is incorrect, merge failed.");
					}
				}
				else
				{
					client.sendToClient("The specified nickname to merge, is not registered at all.");
				}
			}
			else
			{
				client.sendToClient("You have to be authenticated in order to be able to use this feature");
			}
		}
		
		/**
		 * Change the nickname in different states
		 * params[1] == nickname
		 */
		if(command.equals("NICK"))
		{
			if(!checkIfNicknameInUse(params.get(1)))
			{
				if(!this.nickServDB.checkIfRegistered(params.get(1)))
				{
					changeNickname(client, nickname, params.get(1));
					nickname = client.getNickName();
					
					client.setAuthenticated(false);
					client.sendToClient("Nickname successfully changed");
				}
				else
				{
					if(client.isAuthenticated() && this.nickServDB.checkIfOwnerOfNick(nickname, params.get(1)))
					{
						changeNickname(client, nickname, params.get(1));
						nickname = client.getNickName();
						
						client.sendToClient("Nickname successfully changed");
					}
					else
					{
						Thread timer = new Thread(new Timer(client));
						changeNickname(client, nickname, params.get(1));
						timer.start();
					}
				}
			}
			else
			{
				client.sendToClient("That nickname is currently in use");
			}
		}
		
	}
	catch(IOException e)
	{
		System.out.println("Something went horribly wrong");
		e.printStackTrace();
	}
  }
  
  	/**
  	 * There is a problem with synchronisity here.
  	 * @author shokora
  	 *
  	 */
	private class Timer implements Runnable
	{
		private ConnectionToClient client;
		
		public Timer(ConnectionToClient client)
		{
			this.client = client;
		}
		
		@Override
		public void run()
		{
			try
			{
				this.client.setAuthenticating(true);
				this.client.sendToClient("You have 30 seconds to authenticate");
				String nickname = this.client.getNickName();
				for(int i=0;i<30;i++)
				{
					try
					{
						Thread.sleep(1000);
					} catch (InterruptedException e)
					{
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				this.client.setAuthenticating(false);
				
				if(!this.client.isAuthenticated())
				{
	
					String randomNick = generateRandomNickname();
					this.client.sendToClient("30 seconds have passed, try again, your nickname has been set to"+randomNick);
					changeNickname(client, client.getNickName(), generateRandomNickname());
					this.client.setAuthenticated(false);
				}
			}
			catch(IOException e)
			{
				e.printStackTrace();
			}
		}
	}
  
  public boolean checkIfNicknameInUse(String nickname)
  {
	  return currentNicknames.containsKey(nickname);
  }
  
  /**
   * Changes the nickname
   * @param oldnick
   * @param newnick
   * @pre !currentNicknames.contains(newnick)
   */
  public void changeNickname(ConnectionToClient client,String oldnick, String newnick)
  {
	  currentNicknames.remove(oldnick);
	  currentNicknames.put(newnick,true);
	  client.setNickName(newnick);
  }
  
  public String generateRandomNickname()
  {
		boolean uniqueNickname = false;
		String randomNickname = "";
		
		while(!uniqueNickname)
		{
			randomNickname = "guest"+String.valueOf(generator.nextInt()%10000);
			
			if(!checkIfNicknameInUse(randomNickname));
			{
				uniqueNickname = true;
			}
		}
		
		return randomNickname;
  }
  
  /**
   * This method overrides the one in the superclass.  Called
   * when the server starts listening for connections.
   */
  protected void serverStarted()
  {
    System.out.println
      ("Server listening for connections on port " + getPort());
  }

  /**
   * This method overrides the one in the superclass.  Called
   * when the server stops listening for connections.
   */
  protected void serverStopped()
  {
    System.out.println
      ("Server has stopped listening for connections.");
  }
  
  @Override
  protected void serverClosed()
  {
    try
    {
    	FileOutputStream out = new FileOutputStream(FILENAME);
    	ObjectOutputStream outObj = new ObjectOutputStream(out);
    	outObj.writeObject(this.nickServDB);
    }
    catch(IOException e)
    {
    	e.printStackTrace();
    }
  }

  //Class methods ***************************************************

  /**
   * This method is responsible for the creation of
   * the server instance (there is no UI in this phase).
   *
   * @param args[0] The port number to listen on.  Defaults to 5555
   *          if no argument is entered.
   */
  public static void main(String[] args)
  {
    int port = 0; //Port to listen on

    try
    {
      port = Integer.parseInt(args[0]); //Get port from command line
    }
    catch(Throwable t)
    {
      port = DEFAULT_PORT; //Set port to 5555
    }

    EchoServer sv = new EchoServer(port);

    try
    {
      sv.listen(); //Start listening for connections
    }
    catch (Exception ex)
    {
      System.out.println("ERROR - Could not listen for clients!");
    }
  }
}
//End of EchoServer class
